Skip to content

feat: use MCP SDK for recipient resolution - #13

Merged
TheSinding merged 5 commits into
mainfrom
feat/mcp-sdk-recipient-resolution
Aug 4, 2026
Merged

feat: use MCP SDK for recipient resolution#13
TheSinding merged 5 commits into
mainfrom
feat/mcp-sdk-recipient-resolution

Conversation

@TheSinding

Copy link
Copy Markdown
Owner

Summary

  • migrate the stdio MCP server to the official Go SDK
  • resolve recipient phrases for one-to-one chats, group chats, and channels
  • send individually when a requested multi-person group does not exist
  • document the behavior and add a changelog entry

Test

  • rtk go test ./...

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR migrates teamsctl mcp from a hand-rolled JSON-RPC implementation to the official Go MCP SDK and introduces recipient-phrase resolution so tools can address 1:1 chats, existing group chats, and named chats/channels without requiring raw conversation IDs.

Changes:

  • Replace the custom stdio JSON-RPC MCP server with an SDK-based mcp.Server and typed tool registrations.
  • Add recipient-phrase resolution (1:1, group, named chat/channel) with a send-time fallback to individual 1:1 messages when a requested group chat doesn’t exist.
  • Update docs/specs and add a changelog entry; update tests to use SDK in-memory transport.

Reviewed changes

Copilot reviewed 10 out of 11 changed files in this pull request and generated 3 comments.

Show a summary per file
File Description
README.md Documents recipient-phrase behavior for get_messages / send_message.
internal/teamsctl/models.go Removes JSON-RPC request/response models no longer needed with the SDK.
internal/teamsctl/mcp.go Implements the SDK server, typed tools, recipient resolution, and fallback sending logic.
internal/teamsctl/mcp_tools.go Deletes legacy manual tool schema/dispatch implementation.
internal/teamsctl/mcp_tools_test.go Deletes tests tied to the legacy tool-schema implementation.
internal/teamsctl/mcp_test.go Adds SDK-based integration tests for tool listing and auth-error behavior.
internal/teamsctl/conversations_test.go Adds unit tests for recipient parsing/intents and group matching.
go.mod Adds the MCP Go SDK and new indirect dependencies.
go.sum Updates dependency checksums accordingly.
docs/superpowers/specs/2026-08-04-mcp-sdk-recipient-resolution-design.md Adds a design/spec document for the migration + recipient resolution behavior.
CHANGELOG.md Adds an initial changelog entry describing the new MCP SDK + recipient resolution behavior.
Suppressed comments (3)

internal/teamsctl/mcp.go:103

  • With limit now optional, calling FindConversations(..., input.Limit) will pass 0 when the field is omitted, returning all conversations by default. If the input is changed to *int, apply a default (e.g. 50) when nil so agents don't accidentally enumerate everything.
	conversations, err := service.FindConversations(input.Query, input.Kind, input.Limit)
	return nil, conversations, err

internal/teamsctl/mcp.go:32

  • Same as list_conversations: using int for limit makes the default "0" (all messages) when the field is omitted, which can unintentionally fetch an unbounded history. Use *int so you can keep a safe default (e.g. 50) while still allowing 0 explicitly.
type messagesInput struct {
	Recipient      string `json:"recipient,omitempty" jsonschema:"Recipient phrase from the user, such as Mike, Mike and Charlie, ASM group chat, or ASM channel."`
	ConversationID string `json:"conversation_id,omitempty" jsonschema:"Deprecated: use recipient. A Teams conversation ID remains accepted."`
	Limit          int    `json:"limit,omitempty" jsonschema:"Maximum number of messages to return; zero returns all."`
}

internal/teamsctl/mcp.go:136

  • service.Messages(..., input.Limit) will pass 0 when limit is omitted, which returns the full message history by default. If limit becomes *int, apply a default (e.g. 50) when nil so the default stays bounded.
	messages, err := service.Messages(target.IDs, target.Name, input.Limit)
	return nil, messages, err

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread internal/teamsctl/mcp.go
if err != nil {
return nil, nil, err
}
conversation, err := service.findOneOnOneConversation(input.Query)

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in 006a285. Empty or whitespace-only queries now return query is required before any chat lookup.

Comment thread internal/teamsctl/mcp.go
Comment on lines +286 to +288
func looksLikeConversationID(target string) bool {
return strings.ContainsAny(target, ":@,")
}

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in 006a285. IDs must now be comma-separated 19: or 48: Teams conversation IDs; email addresses resolve as recipient names.

Comment thread internal/teamsctl/mcp.go
Comment on lines +18 to +22
type listConversationsInput struct {
Query string `json:"query,omitempty" jsonschema:"Case-insensitive title or team-name substring."`
Kind string `json:"kind,omitempty" jsonschema:"Conversation kind: chat or channel."`
Limit int `json:"limit,omitempty" jsonschema:"Maximum number of conversations to return; zero returns all."`
}

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in 006a285. limit is now optional (*int): omitted defaults to 50, while explicit 0 still requests all results.

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 10 out of 11 changed files in this pull request and generated no new comments.

Suppressed comments (3)

internal/teamsctl/mcp.go:201

  • Recipient resolution intentionally returns a missingGroupChatError for multi-person reads when no existing group chat is found. There is currently no test asserting that get_messages returns this clear error (and does not fall back to multiple 1:1 conversations). Adding a test for this behavior would align with the documented contract.
	if recipients := splitRecipientNames(target); len(recipients) > 1 {
		conversation, err := s.findGroupConversation(recipients)
		if err != nil {
			return conversationTarget{}, err
		}
		if len(conversation.IDs) == 0 {
			return conversationTarget{}, &missingGroupChatError{Recipients: recipients}
		}

internal/teamsctl/mcp.go:158

  • The new missing-group fallback behavior in sendMessage (resolving individuals and sending N separate messages) is not covered by tests. Adding an integration/unit test for the missingGroupChatError path would help prevent regressions in recipient resolution and ensure the fallback reporting fields stay stable.

This issue also appears on line 194 of the same file.

	target, err := service.resolveConversationTarget(firstNonEmpty(input.Recipient, input.ConversationID))
	if err != nil {
		var missingGroup *missingGroupChatError
		if !errors.As(err, &missingGroup) {
			return nil, nil, err
		}
		target, err = service.resolveIndividualTargets(missingGroup.Recipients)
		if err != nil {
			return nil, nil, err
		}
		target.FallbackToOneOnOne = true
	}

docs/superpowers/specs/2026-08-04-mcp-sdk-recipient-resolution-design.md:13

  • The design spec says the MCP server runs through mcp.StdioTransport, but the implementation uses mcp.IOTransport in RunMCP. Update the spec to match the actual transport to avoid future drift and confusion.
`teamsctl mcp` will construct an SDK `mcp.Server`, register typed tools, and
run it through `mcp.StdioTransport`. The SDK owns initialization, protocol
negotiation, JSON-RPC framing, input schema generation, and tool dispatch.

@TheSinding
TheSinding merged commit da572a8 into main Aug 4, 2026
3 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants